其他
8000字 | 32 张图 | 一文搞懂事务+隔离级别+阻塞+死锁
The following article is from 悟空聊架构 Author 悟空聊架构
事务
1.1 什么是事务
1.2 语法
BEGIN` `TRAN``INSERT` `INTO` `b(t1) ``VALUES``(1)``INSERT` `INTO` `b(t1) ``VALUES``(2)``COMMIT` `TRAN
1.3 事务的四个属性ACID
1.事务必须是原子工作单元。事务中进行的修改,要么全部执行,要么全都不执行; 2.在事务完成之前(提交指令被记录到事务日志之前),系统出现故障或重新启动,SQL Server将会撤销在事务中进行的所有修改; 3.事务在处理中遇到错误,SQL Server通常会自动回滚事务; 4.少数不太严重的错误不会引发事务的自动回滚,如主键冲突、锁超时等; 5.可以使用错误处理来捕获第4点提到的错误,并采取某种操作,如把错误记录在日志中,再回滚事务; 6.SELECT @@TRANCOUNT可用在代码的任何位置来判断当前使用SELECT @@TRANCOUNT的地方是否位于一个打开的事务当中,如果不在任何打开的事务范围内,则该函数返回0;如果在某个打开的事务返回范围内,则返回一个大于0的值。打开一个事务,@@TRANCOUNT=@@TRANCOUNT+1;提交一个事务,@@TRANCOUNT-1。
1.同时发生的事务在修改和查询数据时不发生冲突; 2.一致性取决于应用程序的需要。后面会讲到一致性级别,以及如何对一致性进行控制。
1.用于控制数据访问,确保事务只访问处于期望的一致性级别下的数据; 2.使用锁对各个事务之间正在修改和查询的数据进行隔离。
1.「写事务日志:」 在将数据修改写入到磁盘上数据库的数据分区之前会把这些修改写入到磁盘上数据库的事务日志中,把提交指令记录到磁盘的事务日志中以后,即时数据修改还没有应用到磁盘的数据分区,也可以认为事务是持久化的。 2.「系统重新启动:」 正常启动或在发生系统故障之后启动,SQL Server会每个数据库的事务日志,进行回复处理。 3.「恢复处理包含两个阶段:」 重做阶段和撤销阶段。 4.「前滚:」 在重做阶段,对于提交指令已经写入到日志的事务,但数据修改还没有应用到数据分区的事务,数据库引擎会重做这些食物所做的所有修改。 5.「回滚:」 在撤销阶段,对于提交指令没有写入到日志中的事务,数据库引擎会撤销这些事务所做的修改。(这句话需要research,可能是不正确的。因为提交指令没有写入到数据分区,撤销修改是指撤销哪些修改呢?)
锁
2.1 事务中的锁
2.2 锁模式
a.当试图修改数据时,事务只能为所依赖的数据资源请求排他锁。 b.持有排他锁时间:一旦某个事务得到了排他锁,则这个事务将一直持有排他锁直到事务完成。 c.排他锁和其他任何类型的锁在多事务中不能在同一阶段作用于同一个资源。
a.当试图读取数据时,事务默认会为所依赖的数据资源请求共享锁。 b.持有共享锁时间:从事务得到共享锁到读操作完成。 c.多个事务可以在同一阶段用共享锁作用于同一数据资源。 d.在读取数据时,可以对如何处理锁定进行控制。后面隔离级别会讲到如何对锁定进行控制。
2.3 排他锁和共享锁的兼容性
2.4 可锁定的资源的类型
格式: fileid: pagenumber: rid (1:109:0 )
例子:
2.5 锁升级
例如单个语句获得至少5000个锁,就会触发锁升级,如果由于锁冲突而导致无法升级锁,则SQL Server每当获取1250个新锁时出发锁升级。
阻塞
3.1 阻塞
阶段1:事务A请求资源S1,事务不对资源S1进行操作 阶段2:事务A用锁A锁定资源S1,事务B请求对资源S1进行不兼容的锁定(锁B),锁B的请求被阻塞,事务B将进入等待状态 阶段3:事务A正在释放锁A,事务B等待锁A释放, 阶段4:事务A的锁A已释放,事务B用锁B锁定资源S1
3.2 排除阻塞问题
3.2.1 准备工作:
1.准备测试数据
--先创建一张表Product作为测试。id为表的主键,price为product的价格
CREATE TABLE [dbo].[myProduct](
[id] [int] NOT NULL,
[price] [money] NOT NULL
) ON [PRIMARY]
GO
--插入一条数据,id=1,price=10
INSERT INTO [TSQLFundamentals2008].[dbo].[myProduct]([id],[price])VALUES(1,10)
2.模拟阻塞发生的情况
--Connection1
BEGIN TRAN
UPDATE dbo.myProduct SET price = price + 1 WHERE id=1
--Connection2
SELECT * FROM dbo.myProduct WHERE id=1
Connection2结果
--Connection3
SELECT request_session_id AS 会话id ,
resource_type AS 请求锁定的资源类型 ,
resource_description AS 描述 ,
request_mode AS 模式 ,
request_status AS 状态
FROM sys.dm_tran_locks
3.2.2 分析阻塞原因
3.2.2.1 sys.dm_tran_locks 视图
3.2.2.2 sys.dm_exec_connections 视图
SELECT session_id ,
connect_time ,
last_read ,
last_write ,
most_recent_sql_handle
FROM sys.dm_exec_connections
WHERE session_id IN ( 52, 56 )
3.2.2.3 sys.dm_exec_sql_text 表函数
会话52:
执行的SQL语句:
BEGIN TRAN
UPDATE dbo.myProduct
SET price = price + 1
WHERE id = 1
会话56:
执行的SQL语句:
(@1 tinyint)
SELECT * FROM [dbo].[myProduct]
WHERE [id]=@1
SELECT session_id ,
text
FROM sys.dm_exec_connections
CROSS APPLY sys.dm_exec_sql_text
(most_recent_sql_handle) AS ST
WHERE session_id IN ( 52, 56 )
3.2.2.4 sys.dm_exec_sessions 视图
SELECT * FROM sys.dm_exec_sessions
3.2.2.5 sys.dm_exec_requests 视图
SELECT * FROM sys.dm_exec_sessions
3.2.3 解决阻塞问题
3.2.3.1 Lock_TIMEOUT 选项
3.2.3.1 KILL命令
隔离级别
4.1 基本概念
a.隔离级别用于决定如何控制并发用户读写数据的操作
a.任何对表做出修改的语句 b.使用排他锁 c.不能修改读操作获得的锁和锁的持续时间
a.任何检索数据的语句 b.默认使用共享锁 c.使用隔离级别来控制读操作的处理方式
4.2 隔离级别的分类
4.3 隔离级别的设置
SET TRANSACTION ISOLATION LEVEL <isolation name>;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT ... FROM <table> WITH (<isolation name>);
SELECT * FROM dbo.myProduct WITH (READCOMMITTED);
1.设置会话选项的隔离级别时,隔离级别中的每个单词之间需要用空格分隔 2.用表提示的隔离级别时,隔离级别中的每个单词之间不需要用空格分隔 3.表提示的隔离级别有同义词,如:NOLOCK->READUNCOMMITTED,HOLDLOCK->REPEATABLEREAD 4.隔离级别的严格性:1.未提交读<2.已提交读<3.可重复读<4.可序列化 5.隔离级别越高,一致性越高,并发性越低 6.基于快照的隔离级别,SQL Server将提交过的行保存到tempdb数据库中,当读操作发现行的当前版本和它们预期的不一致时,可以立即得到行的以前版本,从而不用请求共享锁也能取得预期的一致性
4.4 隔离级别的行为方式
4.4.1 未提交读 (READ UNCOMMITTED)
Step1: 执行Connection1的阶段2的SQL 语句,然后执行connection2的SQL语句 Step2: 执行Connection1的阶段3的SQL 语句,执行connection2的SQL语句 Step3: 执行Connection1的阶段4的SQL 语句,执行connection2的SQL语句
--阶段2
UPDATE myProduct
SET price = price + 1
WHERE id = 1;
SELECT id ,
price
FROM dbo.myProduct
WHERE id = 1;
--阶段3
UPDATE myProduct
SET price = price + 5
WHERE id = 1;
SELECT id ,
price
FROM dbo.myProduct
WHERE id = 1;
--阶段4
COMMIT TRAN
--在阶段2执行之后
SET TRAN ISOLATION LEVEL READ UNCOMMITTED
BEGIN TRAN;
SELECT id ,
price
FROM dbo.myProduct
WHERE id = 1
COMMIT TRAN;
阶段1:Price=10,事务A对myProduct表请求排他锁 阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1,然后事务A查询price的价格: price=11。事务B不请求任何锁,事务B在A更新Price之后进行查询,price=11 阶段3:事务A更新price = price + 5,然后事务A查询price的价格,price = 16。事务B查询price的价格: price=16 阶段4:事务A释放排他锁 阶段5:事务A中查询price的价格:price = 16。事务B查询price的价格: price=16
4.4.2 已提交读(READ COMMITTED)(默认值)
UPDATE` `dbo.myProduct ``SET` `price = price + 1 ``WHERE` `id=1
SELECT` `* ``FROM` `dbo.myProduct ``WHERE` `id =1
SET` `TRANSACTION` `ISOLATION` `LEVEL` `READ` `COMMITTED
SELECT` `* ``FROM` `dbo.myProduct ``WHERE` `id = 1
阶段1:Price=10,事务A对myProduct表请求排他锁 阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1,然后事务A查询price的价格: price=11。然后事务B请求共享锁进行读操作,查询price,
阶段3:事务A提交事务(COMMIT TRAN) 阶段4:事务A提交完事务后,释放排他锁 阶段5:事务B获得了共享锁,进行读操作,price=11 「“已提交读 (READ UNCOMMITTED)”隔离级别的含义:」
4.4.3 可重复读(REPEATABLE READ)
SET` `TRANSACTION` `ISOLATION` `LEVEL` `REPEATABLE` `READ
SELECT` `* ``FROM` `dbo.myProduct ``WHERE` `id = 1
UPDATE` `dbo.myProduct ``SET` `price = price + 1 ``WHERE` `id=1
阶段1:Price=10,事务A对myProduct表请求共享锁 阶段2:事务A对myProduct表使用了共享锁,事务A查询price的价格: price=10,事务A一直持有共享锁直到事务A完成为止。然后事务B请求排他锁进行写操作price=price+1,
阶段3:事务A查询price, price=10, 说明事务B的更新操作被阻塞了,更新操作没有被执行。然后事务A提交事务(COMMIT TRAN) 阶段4:事务A提交完事务后,释放共享锁 阶段5:事务B获得了排他锁,进行写操作,price=11
4.4.4 可序列化(SERIALIZABLE)
Step1: 执行Connection1的SQL 语句
BEGIN TRANSACTION
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
SELECT * FROM dbo.myProduct WHERE id = 1
INSERT` `INTO` `dbo.myProduct(id, price) ``VALUES` `(1, 20)
阶段1:Price=10,事务A对myProduct表请求共享锁 阶段2:事务A对myProduct表使用了共享锁,事务A查询id=1的price的价格:1行记录,price=10,事务A一直持有共享锁直到事务A完成为止。然后事务B请求排他锁进行插入操作id=1,price=20,
阶段3:事务A查询出id=1的数据只有1行,说明事务B的插入操作被阻塞了,插入操作没有被执行。然后事务A提交事务(COMMIT TRAN) 阶段4:事务A提交完事务后,释放共享锁 阶段5:事务B获得了排他锁,进行插入操作,插入成功,查询出id=1的数据有两条-
4.5 隔离级别总结
死锁
SET` `TRANSACTION` `ISOLATION` `LEVEL` `READ` `COMMITTED
BEGIN` `TRAN
UPDATE` `dbo.myProduct ``SET` `price = price + 1 ``WHERE` `id=1
SELECT` `* ``FROM` `dbo.myOrder ``WHERE` `id =1
SET` `TRANSACTION` `ISOLATION` `LEVEL` `READ` `COMMITTED
BEGIN` `TRAN
UPDATE` `dbo.myOrder ``SET` `customer = ``'ddd'` `WHERE` `id = 1
SELECT` `* ``FROM` `dbo.myProduct ``WHERE` `id = 1
阶段1:Price=10,事务A对myProduct表请求排他锁。Customer = aaa,事务B对myOrder请求排他锁。 阶段2:事务A对myProduct表使用了排他锁,更新price = price + 1。然后事务B对myOrder表使用了排他锁,更新customer=ddd。 阶段3:事务A查询myOrder表,对myOrder表请求共享锁,因为事务A的请求的共享锁与事务B的排他锁冲突,所以事务A被阻塞。然后事务B查询myProduct表,对myProduct表请求共享锁,因为事务B的请求的共享锁与事务A的排他锁冲突,所以事务B被阻塞。 阶段4:事务A等待事务B的排他锁释放,事务B等待事务A的排他锁释放,导致死锁。事务A和事务B都被阻塞了。 阶段5:SQL Server在几秒之内检测到死锁,会选择一个事务作为死锁的牺牲品,终止这个事务,并回滚这个事务所做的操作。在这个例子中,事务A被终止,提示信息:事务(进程 ID 53)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。